package pushup.model;

import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;

import org.eclipse.core.runtime.IPath;

public class Type extends AbstractTypeRelatedEntity {
	
	private boolean stub;
	
	private List<Field> fields;
	private List<Type> children;
	
	// target type, list of fields
	private HashMap<Type, LinkedList<Field>> similarities;
	
	private HashSet<Type> matchesAttempts;
	
	private String name;
	
	private String shortName;

	private IPath path;

	private Package pack;
	
	public void setStub(boolean stub){
		this.stub = stub;
	}
	
	public boolean isStub(){
		return stub;
	}

	public Type(String name, String shortName) {
		this.name = name;
		this.shortName = shortName;
		this.fields = new LinkedList<Field>();
		this.children = new LinkedList<Type>();
		this.similarities = new HashMap<Type, LinkedList<Field>>();
		this.matchesAttempts = new HashSet<Type>();
	}
	
	public void increaseSimilarityWith(Type similarType, Field similarField){
		if(!similarities.containsKey(similarType)){
			similarities.put(similarType, new LinkedList<Field>());
		}
		
		similarities.get(similarType).add(similarField);
	}

	public Type getSuperClass() {
		return relatedType;
	}

	public void setSuperClass(Type superClass) {
		this.relatedType = superClass;
	}

	public String getName() {
		return name;
	}

	public boolean addField(Field field) {
		return fields.add(field);
	}

	public void addChildren(Type type) {
		children.add(type);
	}
	
	public boolean hasSimilarities(){
		return similarities.size() > 0;
	}
	
	public boolean hasAlreadyBeenMatchedWith(Type type){
		return matchesAttempts.contains(type);
	}
	
	public void freeMatchedAttempts(){
		matchesAttempts.clear();
	}

	public void computeSimilarity(Type typeB) {	
		setMatches(typeB);
		
		if(shouldMatch(typeB)){
			// shallow copy that will be modified on the run
			LinkedList<Field> fieldsInB = new LinkedList<Field>(typeB.fields);
			match(typeB, fieldsInB);
		}
	}

	private void setMatches(Type typeB) {
		matchesAttempts.add(typeB);
		typeB.matchesAttempts.add(this);
	}

	private void match(Type typeB, LinkedList<Field> fieldsInB) {
		for(Field fieldInA : fields){ // for each field in this type
			for(Field fieldInB : typeB.fields){ // for each field in the targe type
				boolean isStub = fieldInA.getFieldType().isStub();
				boolean sameFieldType = fieldInA.getFieldType().equals(fieldInB.getFieldType());
				boolean alreadyMatched = !fieldsInB.contains(fieldInB);
				if(!isStub && sameFieldType && !alreadyMatched){
					// increase the similarity with the target type (B) with respect to the current field in A and B
					increaseSimilarity(typeB, fieldInB, fieldInA);
					fieldsInB.remove(fieldInB);
					break;
				}
			}
		}
	}

	private void increaseSimilarity(Type typeB, Field fieldB, Field fieldA) {
		increaseSimilarityWith(typeB, fieldB); // add type B, field B as similarity to A
		typeB.increaseSimilarityWith(this, fieldA); // and type A, field A, as similarity to B
		
	}

	/**
	 * We just care about siblings of user-defined classes
	 * @param typeB the type to check against this one
	 * @return true if we should match this type against the parameter one
	 */
	private boolean shouldMatch(Type typeB) {
		return !isStub()
				&& getSuperClass() != null
				&& getSuperClass().equals(typeB.getSuperClass());
	}

	public HashMap<Type, LinkedList<Field>> getSimilarities() {
		return similarities;
	}
	
	public int getMaxSimilarities(){
		int result = 0;
		
		for(LinkedList<Field> similarityList : similarities.values()){
			if (similarityList.size() > result) {
				result = similarityList.size();
			}
		}
		
		return result;
	}
	
	public int getNumberOfSimilarClasses(){
		return similarities.size(); 
	}
	
	public Type getBestCandidate(){
		Type candidate = null;
		Integer maxSimilarities =  getMaxSimilarities();
		for(Type type : similarities.keySet()){
			if(maxSimilarities == similarities.get(type).size()){
				candidate = type;
				break;
			}
		}
		
		return candidate;
	}

	public void setPath(IPath path) {
		this.path = path;	
	}
	
	public IPath getPath(){
		return path;
	}

	public String getShortName() {
		return shortName;
	}

	public void setPackage(Package pack) {
		this.pack = pack;
	}
	
	public Package getPackage(){
		return pack;
	}

}